﻿using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

using SharpNetwork;
using SharpNetwork.SimpleWebSocket;

namespace SharpBroadcast
{
    public class MediaServer<T> : IMediaServer where T : MediaHandler, new()
    {
        public const int DEFAULT_BUFFER_SIZE = 256;
        public const int DEFAULT_QUEUE_SIZE = 256;

        protected int m_InputPort = 9100;
        protected int m_OutputPort = 9200;

        protected List<string> m_InputWhiteList = new List<string>();
        protected List<string> m_ValidInputList = new List<string>();

        protected Dictionary<string, List<object>> m_WebClients = new Dictionary<string, List<object>>();

        protected Dictionary<string, MediaChannelState> m_States = new Dictionary<string, MediaChannelState>();

        protected HttpListener m_InputServer = null;
        protected Thread m_InputListenerThread = null;

        protected WebSocketServerWrapper m_OutputServer = null;

        protected IClientValidator m_ClientValidator = null;

        protected MediaHandler m_MediaHandler = null;

        private Timer m_Timer = null;

        private IServerLogger m_Logger = null;

        public int OutputBufferSize { get; set; }
        public int OutputQueueSize { get; set; }
        public int CacheQueueSize { get; set; }

        public int InputPort { get { return m_InputServer == null ? 0 : m_InputPort; } }
        public int OutputPort { get { return m_OutputServer == null ? 0 : m_OutputPort; } }

        public IServerLogger Logger { get { return m_Logger; } }

        public MediaServer(IServerLogger logger = null)
        {
            OutputBufferSize = DEFAULT_BUFFER_SIZE;
            OutputQueueSize = DEFAULT_QUEUE_SIZE;
            CacheQueueSize = 0;

            if (logger != null) m_Logger = logger;
            else m_Logger = new ConsoleLogger();

            m_MediaHandler = new T();
            m_MediaHandler.MediaServer = this;
        }

        public static bool Match(string pattern1, string pattern2)
        {
            if (pattern1 == pattern2) return true;
            if (pattern1 == "*" || pattern2 == "*") return true;
            if (pattern1.Length == 0) return pattern2.Length == 0;
            else if (pattern2.Length == 0) return false;

            if (pattern1[0] == '*')
            {
                for (int i = 0; i < pattern2.Length; i++)
                {
                    if (Match(pattern1.Substring(1), pattern2.Substring(i)))
                        return true;
                }
                return false;
            }
            else if (pattern2[0] == '*')
            {
                for (int i = 0; i < pattern1.Length; i++)
                {
                    if (Match(pattern1.Substring(i), pattern2.Substring(1)))
                        return true;
                }
                return false;
            }
            else return pattern1[0] == pattern2[0] && Match(pattern1.Substring(1), pattern2.Substring(1));

        }

        private void StartAutoUpdaeStates(int interval = 1000 * 5)
        {
            if (m_Timer != null) StopAutoUpdaeStates();
            if (m_Timer == null) m_Timer = new Timer(AutoUpdaeStates, this, 200, interval);
        }

        private void StopAutoUpdaeStates()
        {
            if (m_Timer != null)
            {
                m_Timer.Dispose();
                m_Timer = null;
            }
        }

        private void AutoUpdaeStates(object param)
        {
            lock (m_States)
            {
                lock (m_WebClients)
                {
                    foreach (var item in m_States)
                    {
                        if (m_WebClients.ContainsKey(item.Key))
                        {
                            item.Value.ClientCount = m_WebClients[item.Key].Count;
                        }
                    }
                }
            }
        }

        public List<string> GetSourceWhitelist()
        {
            List<string> list = new List<string>();
            lock (m_ValidInputList)
            {
                list.AddRange(m_InputWhiteList);
            }
            return list;
        }

        public void SetSourceWhitelist(List<string> list)
        {
            lock (m_ValidInputList)
            {
                m_InputWhiteList.Clear();
                m_InputWhiteList.AddRange(list);
            }
        }

        public List<MediaChannelState> GetChannelStates()
        {
            List<MediaChannelState> states = new List<MediaChannelState>();
            lock (m_States)
            {
                foreach (var item in m_States)
                {
                    MediaChannelState state = new MediaChannelState(item.Value);
                    states.Add(state);
                }
            }
            return states;
        }

        public void UpdateState(string channelName, MediaChannelState state)
        {
            // update state
            lock (m_States)
            {
                if (m_States.ContainsKey(channelName)) m_States.Remove(channelName); // refresh it
                m_States.Add(channelName, state);
            }
        }

        public void RemoveState(string channelName)
        {
            lock (m_States)
            {
                if (m_States.ContainsKey(channelName)) m_States.Remove(channelName);
            }
        }

        public void SetClientValidator(IClientValidator validator)
        {
            m_ClientValidator = validator;
        }

        public virtual void RemoveClient(object client)
        {
            try
            {
                if (client != null && client is Session)
                {
                    Session session = client as Session;

                    string requestPath = "";
                    var sessionData = WebMessage.GetSessionData(session, true); // make sure session data is ok
                    if (sessionData.ContainsKey("Path")) requestPath = sessionData["Path"].ToString();

                    if (requestPath == null || requestPath.Length <= 0) return;

                    List<string> paramList = new List<string>();
                    string[] parts = requestPath.Split('/');
                    foreach (var part in parts)
                    {
                        if (part.Trim().Length > 0)
                        {
                            paramList.Add(part.Trim());
                        }
                    }
                    if (paramList.Count <= 0) return;

                    string sourceName = paramList.First();
                    if (sourceName != null && sourceName.Length > 0)
                    {
                        List<object> clients = null;
                        lock (m_WebClients)
                        {
                            if (m_WebClients.ContainsKey(sourceName))
                            {
                                clients = m_WebClients[sourceName];
                            }
                        }
                        if (clients != null)
                        {
                            lock (clients)
                            {
                                clients.Remove(session);
                            }
                        }
                    }
                }

            }
            catch (Exception ex)
            {
                Logger.Error("Failed to remove client: " + ex.Message);
            }

        }

        public virtual void ValidateClient(object client)
        {
            Session session = client as Session;
            if (session == null) return;

            Task.Factory.StartNew(() =>
            {
                try
                {
                    string target = "";

                    if (m_ClientValidator != null)
                    {
                        target = m_ClientValidator.Validate(WebMessage.GetSessionData(session, "Path").ToString());
                    }
                    else
                    {
                        List<string> paramList = new List<string>();
                        string[] parts = WebMessage.GetSessionData(session, "Path").ToString().Split('/');
                        foreach (var part in parts)
                        {
                            if (part.Trim().Length > 0)
                            {
                                paramList.Add(part.Trim());
                            }
                        }
                        if (paramList.Count > 0) target = paramList.First();
                    }
                    
                    if (target != null && target.Length > 0)
                    {
                        bool isOK = true;
                        try
                        {
                            byte[] data = m_MediaHandler.GetWelcomeData(target);

                            if (data != null)
                            {
                                session.Send(new WebMessage(data));
                            }

                        }
                        catch { isOK = false; }

                        if (isOK)
                        {
                            List<object> clients = null;
                            lock (m_WebClients)
                            {
                                if (m_WebClients.ContainsKey(target))
                                {
                                    clients = m_WebClients[target];
                                }
                            }
                            if (clients != null)
                            {
                                lock (clients)
                                {
                                    clients.Add(session);
                                }
                            }
                        }

                    }
                    else
                    {
                        session.Close(); // just close it if failed to validate
                    }

                }
                catch (Exception ex)
                {
                    Logger.Error("Validation error: " + ex.Message);
                }
            });

        }

        public List<object> GetClients(string channelName)
        {
            // get clients
            List<object> clients = null;
            lock (m_WebClients)
            {
                if (m_WebClients.ContainsKey(channelName)) clients = m_WebClients[channelName];
                else
                {
                    clients = new List<object>();
                    m_WebClients.Add(channelName, clients);
                }
            }
            return clients;
        }

        public bool Start(int inputPort = 0, int outputPort = 0, List<string> inputWhiteList = null)
        {
            if (m_InputServer != null || m_OutputServer != null) Stop();

            if (inputPort > 0) m_InputPort = inputPort;
            if (outputPort > 0) m_OutputPort = outputPort;

            lock (m_ValidInputList)
            {
                m_ValidInputList.Clear();
                m_InputWhiteList.Clear();

                if (inputWhiteList != null)
                {
                    m_InputWhiteList.AddRange(inputWhiteList);
                }
                else
                {
                    m_InputWhiteList.Add("127.0.0.1");
                }
            }

            lock (m_WebClients)
            {
                foreach (var item in m_WebClients)
                {
                    List<object> clients = item.Value;
                    lock (clients)
                    {
                        clients.Clear();
                    }
                }
                m_WebClients.Clear();
            }

            bool isServerOK = false;

            if (m_InputServer == null)
            {
                try
                {
                    m_InputServer = new HttpListener();

                    m_InputServer.Prefixes.Add(String.Format(@"http://+:{0}/", m_InputPort));

                    m_InputServer.Start();
                    m_InputListenerThread = new Thread(HandleInputRequests);
                    m_InputListenerThread.Start();

                    isServerOK = true;

                }
                catch (Exception ex)
                {
                    try
                    {
                        Stop();
                    }
                    catch { }
                    m_InputServer = null;
                    Logger.Error("HTTP listening error: " + ex.Message);
                }
            }

            if (isServerOK && m_OutputServer == null)
            {
                isServerOK = false;

                try
                {
                    m_OutputServer = new WebSocketServerWrapper(this);
                    m_OutputServer.ServerProxy.Start(m_OutputPort);

                    isServerOK = true;
                }
                catch (Exception ex)
                {
                    try
                    {
                        Stop();
                    }
                    catch { }
                    m_OutputServer = null;
                    Logger.Error("WebSocket listening error: " + ex.Message);
                }
            }

            isServerOK = isServerOK && IsWorking();

            if (isServerOK) StartAutoUpdaeStates();
            else
            {
                try
                {
                    Stop();
                }
                catch { }
                m_InputServer = null;
                m_OutputServer = null;
                Logger.Error("Failed to start server.");
            }

            return isServerOK;
        }

        private void HandleInputRequests()
        {
            while (m_InputServer.IsListening)
            {
                try
                {
                    var context = m_InputServer.BeginGetContext(new AsyncCallback(InputListenerCallback), m_InputServer);
                    context.AsyncWaitHandle.WaitOne();
                }
                catch { }
            }
        }

        private void InputListenerCallback(IAsyncResult ar)
        {
            HttpListener listener = null;
            HttpListenerContext context = null;

            string remoteIp = "";

            try
            {
                listener = ar.AsyncState as HttpListener;
                context = listener.EndGetContext(ar);
                remoteIp = context.Request.RemoteEndPoint.Address.ToString();
            }
            catch (Exception ex)
            {
                Logger.Error("HTTP context error: " + ex.Message);
                return;
            }

            bool isValidAddress = false; // must set whitelist ...
            if (m_InputWhiteList.Count > 0 && remoteIp.Length > 0)
            {
                lock (m_ValidInputList)
                {
                    if (!m_ValidInputList.Contains(remoteIp))
                    {
                        foreach (string pattern in m_InputWhiteList)
                        {
                            if (Match(pattern, remoteIp))
                            {
                                m_ValidInputList.Add(remoteIp);
                                isValidAddress = true;
                                break;
                            }
                        }
                    }
                    else isValidAddress = true;
                }
            }

            try
            {
                if (context != null && remoteIp.Length > 0 && isValidAddress && context.Request.Url.Segments.Length > 1)
                {
                    Thread thread = new Thread(ReceiveInputData);
                    thread.Start(context);
                }
                else if (context != null)
                {
                    context.Response.Close();
                }
            }
            catch (Exception ex)
            {
                Logger.Error("HTTP process error: " + ex.Message);
            }
        }

        protected void ReceiveInputData(object obj)
        {
            HttpListenerContext ctx = obj as HttpListenerContext;
            if (ctx == null) return;
            else ProcessInputData(ctx);
        }

        protected virtual void ProcessInputData(HttpListenerContext ctx)
        {
            m_MediaHandler.HandleInput(ctx);
        }

        public void Stop()
        {
            try
            {
                if (m_InputServer != null && m_InputServer.IsListening)
                {
                    m_InputServer.Stop();
                    if (m_InputListenerThread != null)
                    {
                        m_InputListenerThread.Join();
                        m_InputListenerThread = null;
                    }
                    m_InputServer.Close();
                }
                if (m_InputListenerThread != null)
                {
                    m_InputListenerThread.Join();
                    m_InputListenerThread = null;
                }
                m_InputServer = null;
            }
            catch { }

            try
            {
                if (m_OutputServer != null)
                {
                    m_OutputServer.ServerProxy.Stop();
                }
                m_OutputServer = null;
            }
            catch { }

            lock (m_ValidInputList)
            {
                m_ValidInputList.Clear();
            }

            lock (m_WebClients)
            {
                foreach (var item in m_WebClients)
                {
                    List<object> clients = item.Value;
                    lock (clients)
                    {
                        clients.Clear();
                    }
                }
                m_WebClients.Clear();
            }

            StopAutoUpdaeStates();
        }

        public virtual bool IsWorking()
        {
            return m_InputServer != null && m_InputServer.IsListening
                && m_OutputServer != null && m_OutputServer.ServerProxy.GetState() > 0;
        }

    }
}
